Skip to content

fix(database): link entity extenders at boot so companions hydrate during HTTP requests#74

Merged
markshust merged 2 commits into
marko-php:developfrom
michalbiarda:feature/entity-extender-boot
May 24, 2026
Merged

fix(database): link entity extenders at boot so companions hydrate during HTTP requests#74
markshust merged 2 commits into
marko-php:developfrom
michalbiarda:feature/entity-extender-boot

Conversation

@michalbiarda
Copy link
Copy Markdown
Contributor

Summary

  • EntityMetadataFactory was not registered as a singleton — every resolution created a fresh instance with an empty cache
  • linkExtenders() was only called from SchemaRegistry::registerEntities(), which is invoked exclusively by CLI migration commands (DiffCommand, MigrateCommand)
  • During HTTP requests, $metadata->extenders was always [], so EntityHydrator never attached companion entities

Fix

  • Register EntityMetadataFactory as a singleton in module.php
  • Add a boot callback that discovers all entity classes across vendor, modules, and app, then calls linkExtendersFrom() to populate the extender map once at startup
  • linkExtendersFrom() throws a loud EntityException if an extender references a parent class that cannot be autoloaded, rather than silently ignoring the misconfiguration

Test plan

  • linkExtendersFrom scans entity classes and links extenders to their parents
  • linkExtendersFrom ignores entities without extends
  • linkExtendersFrom throws EntityException when an extender's parent class cannot be autoloaded
  • Full test suite passes: composer test

Closes #73

🤖 Generated with Claude Code

michalbiarda and others added 2 commits May 24, 2026 11:45
…ring HTTP requests

EntityMetadataFactory is now a singleton and a boot callback discovers all
entity classes and calls linkExtendersFrom() so extender metadata is populated
before any repository hydration occurs. Previously linkExtenders() was only
called from CLI migration commands, leaving companions unattached at runtime.

Closes marko-php#73

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @throws EntityException|MissingPrimaryKeyException|ReflectionException
- Promote ReflectionException import
- Use === [] for empty array check
- Add blank lines between logical blocks for readability
- Apply phpcbf multi-line call formatting to new tests

Co-Authored-By: Michał Biarda <1135380+michalbiarda@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@markshust markshust force-pushed the feature/entity-extender-boot branch from 7313833 to 52d5f29 Compare May 24, 2026 15:48
@markshust
Copy link
Copy Markdown
Collaborator

Thanks @michalbiarda — this is a really important fix. The root cause analysis is spot-on: linkExtenders() was only ever called from SchemaRegistry::registerEntities() (CLI-only), so during HTTP requests the extender map was always empty and EntityHydrator silently skipped companion attachment.

The two-part fix is exactly right:

  1. Make EntityMetadataFactory a singleton so the populated extender cache is shared by every Repository / EntityHydrator that the container resolves.
  2. boot callback discovers all entities and links extenders at startup — runs once per request boot, before any repository touches an entity.

I particularly like that linkExtendersFrom() throws loudly via EntityException::extenderParentClassNotFound() when an extender references a missing parent class, rather than letting it silently no-op. That matches the framework's loud-errors philosophy.

Maintainer changes

Small style polish to match conventions in the rest of the file:

  • Added @throws EntityException|MissingPrimaryKeyException|ReflectionException to linkExtendersFrom() (since the method calls new ReflectionClass(), linkExtenders(), and constructs EntityException).
  • Promoted ReflectionException to a use import.
  • Switched count($tableAttrs) === 0 to $tableAttrs === [].
  • Added blank lines between logical blocks for readability — matched the existing style of parse() / linkExtenders() in the same file.
  • Ran phpcbf on the new tests — three multi-line-call style fixes.

Rebased onto current develop, force-pushed to your branch. Full suite green (5145 passing). Merging via merge commit.

Follow-up consideration

Running EntityDiscovery::discoverInVendor/Modules/App() on every request boot does mean a filesystem scan + reflection per request. For projects with many entities this could add measurable boot overhead. Not blocking this fix — correctness over perf — but worth tracking as a future optimization (cached entity-classes list invalidated on file changes, similar to how Laravel does package discovery). I'll let you decide whether that's worth an issue right now.

@markshust markshust merged commit 00b4a23 into marko-php:develop May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entity extender companions are never hydrated during HTTP requests

2 participants